【SwiftUI】タップでセルがセクション間を移動するリストを実装したメモ
概要
大阪オフィスの山田です。絶賛SwiftUI勉強中なので、ObservableObjectやCombineを使いつつ、セクション間を移動できるリストを実装してみました。試行錯誤しながら実装したので、もっと良い方法があるかと思いますが、現時点で学習したことの整理も含め記事にしています。
開発環境
- Xcode: 11.7
- macOS: 10.15.4
作るもの
セルにチェックボックスがあり、チェックすると下のセクションに、チェックを外すと上のセクションに移動するリストです。
モデルを作成
まずモデルを作成します。
class Item: Identifiable, ObservableObject { let id = UUID() let title: String @Published var check: Bool init(title: String, check: Bool) { self.title = title self.check = check } // 表示確認用データを作成 static func make() -> [Item] { return [ Item(title: "(´・ω・`)", check: true), Item(title: "( ᐛ?)パァ", check: true), Item(title: "( ᐛ?)ニャア", check: false), Item(title: "٩( ᐛ )و.", check: false), Item(title: "( ´・ω・`)人(´・ω・` )", check: false) ] } }
Identifiable
を継承しているのは、ForEach
で使うためです。ObservableObject
を継承しているのは、check
プロパティの変更を検出するためです。make
メソッドは検証に使うテストデータを作るメソッドです。
セルのViewとViewModelを作成
class ItemCellViewModel: ObservableObject { @Published var item: Item @Published var stateSystemName: String = "" private var bag = [AnyCancellable]() init(item: Item) { self.item = item item.$check.map { $0 ? "checkmark.square" : "square" } .assign(to: \.stateSystemName , on: self) .store(in: &bag) } func switchItem() { item.check.toggle() } } struct ItemCell: View { @ObservedObject var vm: ItemCellViewModel var body: some View { HStack { Image(systemName: vm.stateSystemName) .onTapGesture { self.vm.switchItem() } Text(vm.item.title) } } }
ItemCellViewModel
はモデルItem
を生成時に受け取ります。stateSystemName
は、セルに表示するチェックボックスの画像名です。item
のcheck
プロパティに変更があった場合に、stateSystemName
を適切なものに変更しています。map
によりBoolをStringに変換して、assign
メソッドを使ってstateSystemName
変数に代入しています。ItemCell
はセル内の画像をタップした際に、ViewModel
のswitchItem
を実行してcheck
のON/OFFを切り替えています。
ContentViewとContentViewModelの作成
struct ContentView: View { @ObservedObject var vm = ContentViewModel() var body: some View { List { Section(header: Text("Check False")) { ForEach(vm.items.filter { $0.check == false }) { item in ItemCell(vm: ItemCellViewModel(item: item)) } } Section(header: Text("Check True")) { ForEach(vm.items.filter { $0.check == true }) { item in ItemCell(vm: ItemCellViewModel(item: item)) } } } } } class ContentViewModel: ObservableObject { let items = Item.make() private var bag = [AnyCancellable]() init() { items.forEach { [weak self] item in guard let strongSelf = self else { return } item.objectWillChange.sink { strongSelf.objectWillChange.send() }.store(in: &strongSelf.bag) } } }
ContentViewModel
はItem
の配列を持ちます。ContentView
ではcheck
のon/offによってI
Itemをどのセクションに表示するか分けています。ContentViewModel
の初期化処理で、配列のItem
のうち、どれかに変更があった場合、ContentViewModel
でobjectWillChenge.send()
を実行し、変更があったことを通知するようにしています。この処理を入れないと、ContentView
に変更が通知されないため、チェックボックスのON/OFFを切り替えても、チェックボックスの画像は切り替わりますが、セルがSection
間を移動しません。
終わり
今回、この記事を書ききるまで、結構、試行錯誤しました。
ModelをObservableObject
を適用するかどうかを考えたのですが、CoreDataで使うNSManagedObject
もObservableObject
が適用されているので、それに倣いました。
EnvironmentObject
を使うことも考えましたが、Viewの階層も深くないし、まだ僕自身の理解も浅いので、現時点では使用を見送りました。
まだまだ勉強不足だなぁ。実装たのしー。